﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web;
using System.Xml;

using Yawf.Core;
using Yawf.Core.Exceptions;
using Yawf.Core.Reflection;
using Yawf.Security.Entities;
using Yawf.Web.Managers.Entities;
using Yawf.Web.Utils;
using Yawf.Core.Utils;

namespace Yawf.Web.Managers.Modules {
	public class MethodInvokeModule : IModule{
		private IObjectFinder objectFinder;
		private IList<String> classPaths;

		// Properties
		public IObjectFinder ObjectFinder { get { return objectFinder; } set { objectFinder = value; } }
		public IList<String> ClassPaths { get { return classPaths; } set { classPaths = value; } }

		/// <summary>
		/// This method will interpret a method-invoke class and execute a method
		/// on a Spring Object or create a new instance of a class and execute a
		/// method. 
		/// </summary>
		/// <param name="elem">XmlElement of method-invoke tag</param>
		/// <param name="requestWrapper">RequestWrapper object</param>
		/// <param name="responseWrapper">ResponseWrapper object</param>
		/// <param name="user">currently logged on user, could be null</param>
		public void Execute(XmlElement elem,RequestWrapper requestWrapper,ResponseWrapper responseWrapper,IUser user) {
			RequestDataMaterializer materializer;
			MethodInfo method;
			MethodData data;
			Object[] parameters;
			Object invokeOnObj,result;
			String tagName;

			// Read Method data
			data = new MethodData(elem);

			// Get Spring Object or Instanciate Object with Class Name
			invokeOnObj = null;
			if (!StringUtils.IsEmpty(data.SpringBean)) {
				invokeOnObj = Initializer.Instance.GetObject(data.SpringBean);
			} else {
				// Inistanciate Object
				invokeOnObj = ReflectionUtils.CreateInstance(data.ClassName);
			}
			if (invokeOnObj == null) {
				return;
			}
			if (!StringUtils.IsEmpty(data.MethodName)) {
				// Create Materializer
				materializer = new RequestDataMaterializer(requestWrapper, ObjectFinder, ClassPaths);

				// Find best method
				method = FindBestMethod(invokeOnObj, data);

				// Generate method parameters for method we just found
				parameters = GenerateParameters(method, materializer, data);

				// Invoke method
				try {
					result = method.Invoke(invokeOnObj, parameters);
				} catch (TargetInvocationException e) {
					throw new YawfException("Error while invoking method: " + method.ToString(),e.InnerException);
				}
			} else {
				// no method to execute, return the target object
				result = invokeOnObj;
			}

			if (result != null) {
				// Add to ResponseDataCollector
				tagName = "";
				if (data.ResultName != null) {
					tagName = data.ResultName;
				}
				responseWrapper.Add(result,tagName);
			}
		}									// Execute
					
		/// <summary>
		/// Finds best method to invoke, given a MethodData found in XmlElement
		/// </summary>
		/// <param name="theObject">Object to find method to invoke on</param>
		/// <param name="data">MethodData object with MethodName, parameters etc</param>
		/// <returns>A MethodInfo object</returns>
		private MethodInfo FindBestMethod(Object theObject,MethodData data) {
			MethodInfo method = null,tmpMethod;
			IList<MethodInfo> methods;
			ParameterInfo[] parameters;
			int i;

			methods = ReflectionUtils.FindMethodsByName(theObject,data.MethodName,data.Parameters.Count);
			if (methods == null || methods.Count < 1){
				throw new YawfException("Method " + data.MethodName + " not found on object of type " + theObject.GetType());
			}
			if (methods.Count > 1){
				// Find best method according to parameter type
				for(i = 0;i < methods.Count;i++){
					tmpMethod = methods[i];
					parameters = tmpMethod.GetParameters();
					if (AreAllParamsEquals(parameters,data.Parameters)) {
						method = tmpMethod;
					}
				}
				if (method == null){
					throw new YawfException("No suitable " + data.MethodName + " method with correct type of parameters found");
				}
			}else{
				method = methods[0];
			}

			return method;
		}											// FindBestMethod

		/// <summary>
		/// Checks weather all parameters are equals, eg that there are no
		/// differences(types) in those parameters that are declared in Xml and
		/// those that the method declares
		/// </summary>
		/// <param name="parameters">Array of parameters declared by method</param>
		/// <param name="paramsData">List of declared parameters in XML</param>
		/// <returns>true if there are no differences, false otherwise</returns>
		private bool AreAllParamsEquals(ParameterInfo[] parameters,IList<ParameterData> paramsData) {
			ParameterData paramData;
			ParameterInfo param;
			Type tmpType;
			String fullTypeName;
			int i;

			// Number of parameters are different, return false
			if (parameters.Length != paramsData.Count) {
				return false;
			}

			// Loop through all parameters and check for differences in Type
			for(i = 0; i < parameters.Length;i++){
				paramData = paramsData[i];
				param = parameters[i];
				if (paramData.Type != null && paramData.Type.Trim() != "") {
					if (ReflectionUtils.IsSimpleType(paramData.Type)){
						fullTypeName = ReflectionUtils.GetFullTypeName(paramData.Type);
						if (fullTypeName != param.ParameterType.FullName) {
							return false;
						}
					}else{
						tmpType = ReflectionUtils.GetType(paramData.Type);
						if (tmpType == null || !param.ParameterType.IsAssignableFrom(tmpType)) {
							return false;
						}
					}
				}
			}

			return true;
		}									// AreAllParamsEquals

		/// <summary>
		/// Generates parameters using a materializer to get parameters from 
		/// request wrapper
		/// </summary>
		/// <param name="invokeOnObj">Object to we will </param>
		/// <param name="materializer">RequestDataMaterializer to use when materialize objects from request</param>
		/// <param name="data">Method Data</param>
		/// <returns>An array of objects that can be use as parameters when invoking a method</returns>
		private Object[] GenerateParameters(MethodInfo method,RequestDataMaterializer materializer,MethodData data) {
			IList<ParameterData> paramsData;
			ParameterInfo[] parameters;
			Object[] generatedParams = null;
			int i;

			paramsData = data.Parameters;
			if (paramsData != null && paramsData.Count > 0){
				// Loop through all parameter data and generate object values
				generatedParams = new Object[paramsData.Count];
				parameters = method.GetParameters();
				for (i = 0; i < paramsData.Count; i++) {
					generatedParams[i] = GenerateParameter(materializer,paramsData[i],parameters[i]);
				}
			}

			return generatedParams;
		}												// GenerateParameters

		/**
		* Gets the value of a value parameter
		*
		* @param type - type name of class of parameter
		* @param paramData - data of parameter
		* @return An object containing paramater value
		*
		**/
		private Object GetValueParameter(String type,ParameterData paramData) {
			if (!ReflectionUtils.IsSimpleType(type)) {
				throw new YawfException("Parameter " + paramData.Name + " must be a primitive type if you want to use it as a VALUE origin");
			}
			return ReflectionUtils.GetSimpleType(type,paramData.Value);
		}									// GetValueParameter

		/// <summary>
		/// Checks weather a parameter type is an Uploaded File, if the parameter Type
		/// is a Stream or a FileInfo
		/// </summary>
		/// <param name="paramType">Type to check</param>
		/// <returns>True if type is a Stream or</returns>
		private bool IsUploadedFile(Type paramType) {
			if (paramType != null){
				return typeof(Stream).IsAssignableFrom(paramType);
			}
			
			return false;
		}									// IsUploadedFile

		/// <summary>
		/// Gets file upload object
		/// </summary>
		/// <param name="materializer">Materializer object to get parameter data from</param>
		/// <param name="paramType">Type of parameter</param>
		/// <param name="paramData">Parameter Data taken from XML</param>
		/// <returns>An Stream, null if object not found in request</returns>
		private Stream GetUploadedFile(RequestDataMaterializer materializer,Type paramType,ParameterData paramData) {
			HttpPostedFile uploadedFile;
			Stream uploadedStream = null;

			if (IsUploadedFile(paramType) && paramData != null) {
				uploadedFile = materializer.RequestWrapper.GetFile(paramData.Name);
				if (uploadedFile != null){
					uploadedStream = uploadedFile.InputStream;
				}
			}

			return uploadedStream;
		}								// GetUploadedFile

		/// <summary>
		/// Retrieves an parameter from request, could be a simple type or a complex class
		/// </summary>
		/// <param name="materializer">RequestDataMaterializer to use for retrieval</param>
		/// <param name="paramData">Parameter data taken from method invocation XML</param>
		/// <param name="paramType">Type of parameter</param>
		/// <returns>An object retrieved from request, null if not found in request</returns>
		private object GetObjectFromRequest(RequestDataMaterializer materializer,ParameterData paramData,Type paramType) {
			Object tmpObject;
			String typeFullName;
			
			typeFullName = paramType.FullName;
			if (paramType.IsArray && typeFullName.EndsWith("[]")) {
				typeFullName = typeFullName.Remove(typeFullName.Length-2);
			}

			if (ReflectionUtils.IsSimpleType(typeFullName) && !paramType.IsArray) {
				tmpObject = materializer.GetObject(paramData.Name,paramType.FullName,paramData.Value);
			} else if (ReflectionUtils.IsSimpleType(typeFullName) && paramType.IsArray) {
				tmpObject = materializer.GetSimpleTypeArray(paramData.Name,typeFullName);
			} else if (IsUploadedFile(paramType)) {
				tmpObject = GetUploadedFile(materializer,paramType,paramData);
			} else {
				if (!ReflectionUtils.IsConstructable(paramType)) {
					if (paramData.Type == null || paramData.Type.Trim() == "") {
						throw new YawfException("Parameter " + paramData.Name + "is an interface or an abstract class, type attribute must be defined");
					}
				}
				tmpObject = materializer.GetObject(paramData.Name,paramType.AssemblyQualifiedName,paramData.Value);
			}

			return tmpObject;
		}								// GetObjectFromRequest

		/// <summary>
		/// Generates a value taking parameter data from given ParameterData object and materialize
		/// values from Request through materializer
		/// </summary>
		/// <param name="materializer">Materializer object to get parameter data from</param>
		/// <param name="paramData">ParameterData object to get param, name etc</param>
		/// <param name="paramInfo">ParameterInfo to get Type, etc</param>
		/// <returns></returns>
		private object GenerateParameter(RequestDataMaterializer materializer,ParameterData paramData,ParameterInfo paramInfo) {
			Object tmpObject = null;
			String origin;

			if (paramInfo == null) {
				throw new YawfException("No type information exists for parameter " + paramData.Name);
			}

			origin = paramData.Origin;
			if (ParameterData.SPRING_ORIGIN == origin) {
				tmpObject = Initializer.Instance.GetObject(paramData.Name);
			} else if (ParameterData.VALUE_ORIGIN == origin) {
				tmpObject = GetValueParameter(paramInfo.ParameterType.FullName,paramData);
			} else if (ParameterData.DATA_FETCHER_ORIGIN == origin) {
				tmpObject = materializer.RequestWrapper;
			} else if (ParameterData.REQUEST_ORIGIN == origin) {
				tmpObject = GetObjectFromRequest(materializer,paramData,paramInfo.ParameterType);
			}

			return tmpObject;
		}									// GenerateParameter
	}						// Class
}
